# frozen_string_literal: true

default_platform(:ios)

fastlane_require 'xcodeproj'
fastlane_require 'dotenv'
fastlane_require 'open-uri'
fastlane_require 'buildkit'

UI.user_error!('Please run fastlane via `bundle exec`') unless FastlaneCore::Helper.bundler?

PROJECT_ROOT_FOLDER = File.dirname(File.expand_path(__dir__))
VERSION_FILE_PATH = File.join(PROJECT_ROOT_FOLDER, 'config', 'Version.Public.xcconfig')
OUTPUT_DIRECTORY_PATH = File.join(PROJECT_ROOT_FOLDER, 'build', 'results')
APP_STORE_BUNDLE_IDENTIFIER = 'com.codality.NotationalFlow'
DEFAULT_BRANCH = 'trunk'

TEAM_ID_APP_STORE_CONNECT = 'PZYM8XX95Q'
TEAM_ID_ENTERPRISE = '99KV9Z6BKV'

ORGANIZATION = 'automattic'
PROJECT_SLUG = 'simplenote-ios'
GITHUB_REPO = "#{ORGANIZATION}/#{PROJECT_SLUG}".freeze
BUILDKITE_ORGANIZATION = ORGANIZATION
BUILDKITE_PIPELINE = PROJECT_SLUG

FIREBASE_APP_ID = '1:124902176124:ios:d21314b0300402801620f9'
FIREBASE_TESTERS_GROUP = 'simplenote-ios---prototype-builds'

PROJECT = 'Simplenote.xcodeproj'

VERSION_CALCULATOR = Fastlane::Wpmreleasetoolkit::Versioning::SemanticVersionCalculator.new
VERSION_FORMATTER = Fastlane::Wpmreleasetoolkit::Versioning::FourPartVersionFormatter.new
BUILD_CODE_FORMATTER = Fastlane::Wpmreleasetoolkit::Versioning::FourPartBuildCodeFormatter.new
PUBLIC_VERSION_FILE = Fastlane::Wpmreleasetoolkit::Versioning::IOSVersionFile.new(xcconfig_path: VERSION_FILE_PATH)

SECRETS_ROOT = File.join(Dir.home, '.configure', 'simplenote-ios', 'secrets')
APP_STORE_CONNECT_KEY_PATH = File.join(SECRETS_ROOT, 'app_store_connect_fastlane_api_key.json')
$used_test_account_index = nil

GLOTPRESS_BASE_URL = 'https://translate.wordpress.com/projects'
# Notice the trailing / is required.
# Without it, GlotPress will redirect to the version with /
GLOTPRESS_APP_STRINGS_PROJECT_URL = "#{GLOTPRESS_BASE_URL}/simplenote/ios/".freeze
GLOTPRESS_STORE_METADATA_PROJECT_URL = "#{GLOTPRESS_APP_STRINGS_PROJECT_URL}release-notes/".freeze

APP_RESOURCES_DIR = File.join(PROJECT_ROOT_FOLDER, 'Simplenote', 'Resources')
STORE_METADATA_FOLDER = File.join(PROJECT_ROOT_FOLDER, 'fastlane', 'metadata')
STORE_METADATA_DEFAULT_LOCALE_FOLDER = File.join(STORE_METADATA_FOLDER, 'default')
RELEASE_NOTES_SOURCE_PATH = File.join(STORE_METADATA_DEFAULT_LOCALE_FOLDER, 'release_notes.txt')

require_relative 'lib/env_manager'

# Important: These need to be imported after all the constants have been defined because they access them.
# A bit of a leaky abstraction but makes for lanes that are easier to write...
import 'lanes/build.rb'
import 'lanes/localization.rb'
import 'lanes/release.rb'
# This helper is only used in the release lanes but it needs to be imported here in order to access Fastlane-specific API and our methods like release_version_current
import 'lib/release_helpers.rb'

before_all do
  # Ensure we use the latest version of the toolkit
  check_for_toolkit_updates unless is_ci || ENV['FASTLANE_SKIP_TOOLKIT_UPDATE_CHECK']

  EnvManager.set_up(env_file_name: 'simplenote-ios')

  setup_ci
end

error do |lane, _exception, _options|
  deoccupy_test_account if lane == :run_ui_tests
end

########################################################################
# Environment
########################################################################
ENV['PROJECT_NAME'] = 'Simplenote'
ENV['PROJECT_ROOT_FOLDER'] = PROJECT_ROOT_FOLDER
ENV['PUBLIC_CONFIG_FILE'] = VERSION_FILE_PATH
ENV['FL_RELEASE_TOOLKIT_DEFAULT_BRANCH'] = 'trunk'

platform :ios do
  # Upload the localized metadata (from `fastlane/metadata/`) to App Store Connect
  #
  # @option [Boolean] with_screenshots (default: false) If true, will also upload the latest screenshot files to ASC
  #
  desc 'Upload the localized metadata to App Store Connect, optionally including screenshots.'
  lane :update_metadata_on_app_store_connect do |skip_confirm: false, with_screenshots: false|
    # Skip screenshots by default. The naming is "with" to make it clear that
    # callers need to opt-in to adding screenshots. The naming of the deliver
    # (upload_to_app_store) parameter, on the other hand, uses the skip verb.
    skip_screenshots = !with_screenshots

    upload_to_app_store(
      app_identifier: APP_STORE_BUNDLE_IDENTIFIER,
      app_version: release_version_current,
      skip_binary_upload: true,
      screenshots_path: promo_screenshots_directory,
      skip_screenshots: skip_screenshots,
      overwrite_screenshots: true, # won't have effect if `skip_screenshots` is true
      phased_release: true,
      precheck_include_in_app_purchases: false,
      api_key_path: APP_STORE_CONNECT_KEY_PATH,
      force: skip_confirm,
      copyright: "© #{Time.now.year} Automattic, Inc."
    )
  end

  # Build the app and upload it to Firebase App Distribution for adhoc testing
  #
  desc 'Builds and uploads a prototype build'
  lane :build_and_upload_prototype_build do
    update_certs_and_profiles_enterprise

    pr_or_branch = pull_request_number&.then { |num| "PR ##{num}" } || ENV.fetch('BUILDKITE_BRANCH', nil)
    build_number = ENV.fetch('BUILDKITE_BUILD_NUMBER', '0')

    gym(
      scheme: 'Simplenote',
      configuration: 'Distribution Alpha',
      project: PROJECT,
      export_method: 'enterprise',
      clean: true,
      xcargs: { VERSION_SHORT: pr_or_branch, VERSION_LONG: build_number },
      output_directory: OUTPUT_DIRECTORY_PATH,
      output_name: 'Simplenote Alpha',
      export_team_id: TEAM_ID_ENTERPRISE,
      export_options: {
        method: 'enterprise',
        provisioningProfiles: simplenote_provisioning_profiles(
          root_bundle_id: "#{APP_STORE_BUNDLE_IDENTIFIER}.Alpha",
          match_type: 'InHouse'
        )
      }
    )

    release_notes = <<~NOTES
      Pull Request: ##{pull_request_number || 'N/A'}
      Branch: `#{ENV.fetch('BUILDKITE_BRANCH', 'N/A')}`
      Commit: #{ENV.fetch('BUILDKITE_COMMIT', 'N/A')[0...7]}
    NOTES

    firebase_app_distribution(
      app: FIREBASE_APP_ID,
      service_credentials_json_data: EnvManager.get_required_env!('FIREBASE_APP_DISTRIBUTION_ACCOUNT_KEY'),
      release_notes: release_notes,
      groups: FIREBASE_TESTERS_GROUP
    )

    sentry_upload_dsym(
      auth_token: EnvManager.get_required_env!('SENTRY_AUTH_TOKEN'),
      org_slug: 'a8c',
      project_slug: 'simplenote-ios',
      dsym_path: lane_context[SharedValues::DSYM_OUTPUT_PATH]
    )

    next if pull_request_number.nil?

    # Post PR Comment
    comment_body = prototype_build_details_comment(
      app_display_name: 'Simplenote Prototype Build',
      fold: true
    )
    comment_on_pr(
      project: GITHUB_REPO,
      pr_number: pull_request_number,
      reuse_identifier: 'installable-build-link--Simplenote-Installable-Builds',
      body: comment_body
    )
  end

  #####################################################################################
  # Screenshot lanes
  #####################################################################################

  # This is ideal when running locally. On CI, it's better to run each lane in
  # isolation, to leverage parallelism if necessary.
  #
  # On the other hand, right now Simplenote only needs one locale, so we might
  # as well keep using this on CI too. We still benefit from not having to
  # rebuild the app just to take the screenshots in light vs dark mode.
  desc 'Walk through the app taking screenshots.'
  lane :take_screenshots do |options|
    build_app_for_screenshots(options)

    # In order to preserve both light and dark screenshots, we won't be erasing
    # the screenshots folder in between captures but only on the first one.
    take_screenshots_from_app(
      options.merge({ mode: 'light', clear_previous_screenshots: true })
    )
    take_screenshots_from_app(
      options.merge({ mode: 'dark', clear_previous_screenshots: false })
    )
  end

  desc 'Build the binaries to run to take the screenshots'
  lane :build_app_for_screenshots do
    scan(
      project: project_path,
      scheme: screenshots_scheme,
      build_for_testing: true,
      derived_data_path: derived_data_directory
    )
  end

  desc 'Runs through the app taking screenshots, using a prebuilt binary'
  lane :take_screenshots_from_app do |options|
    devices = (options[:devices] || screenshot_devices).split(',').flatten

    languages = [
      'en-US'
    ]

    # Erase the Simulators between runs in order to get everything back to a
    # default state. This should also compensate for default CI installations
    # not having the Simulators need, by building them.
    rebuild_screenshot_devices(
      devices: devices,
      simulator_version: simulator_version
    )

    capture_ios_screenshots(
      scheme: screenshots_scheme,

      localize_simulator: true,
      languages: languages,

      devices: devices,

      # Don't rebuild the app, use the binaries from the given DerivedData
      # folder. This is so that we can parallelize test runs with multiple
      # device and locale combinations.
      test_without_building: true,
      derived_data_path: derived_data_directory,

      output_directory: screenshots_directory,
      clear_previous_screenshots: options.fetch(:clear_previous_screenshots, true),

      # Explicitly set the iOS version to ensure we match the Simulators we
      # recreated above
      ios_version: simulator_version,

      # Retry a few times if something is a little flaky
      number_of_retries: 3,

      # But fail completely after those 3 retries
      stop_after_first_error: true,

      # Run one Simulator at a time. One of the screenshots requires adding
      # text to a note but because we're using the same account across all the
      # Simulators, editing the note on one will result in the changes
      # appearing on the other making for unexpected and inconsistent
      # screnshots
      concurrent_simulators: false,

      # Allow the caller to invoke dark mode
      dark_mode: options[:mode].to_s.downcase == 'dark'
    )
  end

  lane :create_promo_screenshots do |options|
    unless Fastlane::Helper::GitHelper.has_git_lfs
      UI.user_error!('Git LFS not enabled – Unable to generate promo screenshots. Run `git lfs install && git lfs fetch && git lfs pull` to fix this.')
    end

    # This value is defined in style.css. It would be good if there was a way
    # to make it parametric, so that if we update the CSS we don't risk this
    # getting out of sync.
    font_name = 'SourceSansPro-Regular.ttf'
    user_font_directory = File.join(Dir.home, 'Library/Fonts')
    user_font_path = File.join(user_font_directory, font_name)
    if File.exist?(user_font_path)
      UI.success("Custom font #{font_name} already installed locally.")
    else
      UI.message("Installing #{font_name} at #{user_font_path}.")
      `mkdir -p #{user_font_directory}`
      `cp #{File.join(Dir.pwd, "appstoreres/assets/#{font_name}")} #{user_font_path}`
    end

    promo_screenshots(
      orig_folder: options[:source] || screenshots_directory,
      metadata_folder: File.join(Dir.pwd, 'metadata'),
      output_folder: promo_screenshots_directory,
      force: options[:force] || true
    )
  end

  desc 'Rebuild Screenshot Devices'
  lane :rebuild_screenshot_devices do |options|
    require 'simctl'

    # Using flatten here because we may be getting a comma separated string if
    # called from the command line via "fastlane run" or an array if called by
    # another action that has already preformatted the value into an array.
    device_names = (options[:devices] || screenshot_devices).split(',').flatten
    sim_version = options[:simulator_version] || simulator_version

    SimCtl.list_devices.each do |device|
      next unless device_names.include? device.name

      UI.message("Deleting #{device.name} because it already exists.")
      device.delete
    end

    device_names.each do |device|
      runtime = SimCtl.runtime(name: "iOS #{sim_version}")
      devicetype = SimCtl.devicetype(name: device)

      SimCtl.create_device device, devicetype, runtime
    end
  end

  #####################################################################################
  # Test lanes
  #####################################################################################

  # Props to work done by others in this file and in
  # https://github.com/wordpress-mobile/WordPress-iOS/blob/trunk/fastlane/Fastfile
  # this is a combined rip-off from both

  #####################################################################################
  # test
  # -----------------------------------------------------------------------------------
  # Run unit tests
  # -----------------------------------------------------------------------------------
  # Usage:
  # bundle exec fastlane test [device:<Name of the iOS Simulator>]
  #
  # Example:
  # bundle exec fastlane test device:"iPhone 14"
  #####################################################################################
  desc 'Run Unit Tests'
  lane :run_unit_tests do |options|
    scan(
      project: project_name,
      scheme: 'Simplenote',
      device: options[:device] || 'iPhone 14',
      output_directory: OUTPUT_DIRECTORY_PATH,
      reset_simulator: true,
      result_bundle: true
    )
  end

  #####################################################################################
  # run_ui_tests
  # -----------------------------------------------------------------------------------
  # A lane to run a subset of automated UI tests. Since only login tests are
  # selected for now, there will be no credentials race condition.
  # -----------------------------------------------------------------------------------
  # Usage:
  # bundle exec fastlane run_ui_tests [scheme:<Xcode scheme with UI tests>] [device:<Name of the iOS Simulator>] [test_account:<Test account email>]
  #
  # Example:
  # bundle exec fastlane run_ui_tests scheme:"SimplenoteUITests_Subset" device:"iPhone 14" test_account:"test.account@test.com"
  #####################################################################################
  desc 'Run UI tests'
  lane :run_ui_tests do |options|
    scan(
      project: project_name,
      scheme: options[:scheme] || 'SimplenoteUITests_Subset',
      device: options[:device] || 'iPhone 14',
      output_directory: OUTPUT_DIRECTORY_PATH,
      reset_simulator: true,
      xcargs: { UI_TEST_ACCOUNT: options[:test_account] || '' },
      result_bundle: true
    )
  end

  #####################################################################################
  # pick_test_account_and_run_ui_tests
  # -----------------------------------------------------------------------------------
  # Firstly tries to occupy a free test account, and then passes the account to
  # `run_ui_tests` lane. Frees up the account afterwards.
  # -----------------------------------------------------------------------------------
  # Usage:
  # bundle exec fastlane pick_test_account_and_run_ui_tests [device:<Name of the iOS Simulator>]
  #
  # Example:
  # bundle exec fastlane pick_test_account_and_run_ui_tests device:"iPhone 14"
  #####################################################################################
  desc 'Occupy a free test account and run UI tests with it'
  lane :pick_test_account_and_run_ui_tests do |options|
    if is_ci
      sanitize_test_accounts
      find_free_test_account
      run_ui_tests(options.merge({ test_account: EnvManager.get_required_env!('UI_TESTS_ACCOUNT_EMAIL').sub('X', $used_test_account_index.to_s) }))
      deoccupy_test_account
    else
      UI.user_error!('This lane should be run only from CI')
    end
  end
end

########################################################################
# Fastlane Match Code Signing Lanes
########################################################################

# Downloads all the required certificates and profiles for both production and internal distribution builds.
# Optionally, it can create any new necessary certificates or profiles.
#
# @option [Boolean] readonly (default: true) Whether to only fetch existing certificates and profiles, without generating new ones.
lane :update_certs_and_profiles do |readonly: true|
  update_certs_and_profiles_enterprise(readonly: readonly)
  update_certs_and_profiles_app_store(readonly: readonly)
end

# Downloads all the required certificates and profiles (using `match`) for the alpha builds in the enterprise account.
# Optionally, it can create any new necessary certificates or profiles.
#
# @option [Boolean] readonly (default: true) Whether to only fetch existing certificates and profiles, without generating new ones.
#
lane :update_certs_and_profiles_enterprise do |readonly: true|
  update_code_signing_enterprise(
    app_identifiers: simplenote_app_identifiers(root_bundle_id: "#{APP_STORE_BUNDLE_IDENTIFIER}.Alpha"),
    readonly: readonly
  )
end

# Downloads all the required certificates and profiles for the production build.
# Optionally, it can create any new necessary certificates or profiles.
#
# @option [Boolean] readonly (default: true) Whether to only fetch existing certificates and profiles, without generating new ones.
lane :update_certs_and_profiles_app_store do |readonly: true|
  update_code_signing(
    type: 'appstore',
    team_id: TEAM_ID_APP_STORE_CONNECT,
    readonly: readonly,
    app_identifiers: simplenote_app_identifiers,
    api_key_path: APP_STORE_CONNECT_KEY_PATH,
    template_name: 'NotationalFlow Keychain Access (Distribution)'
  )
end

def update_code_signing_enterprise(readonly:, app_identifiers:)
  if readonly
    # In readonly mode, we can use the API key
    api_key_path = APP_STORE_CONNECT_KEY_PATH
  else
    # The Enterprise account APIs do not support authentication via API key.
    # If we want to modify data (readonly = false) we need to authenticate manually.
    prompt_user_for_app_store_connect_credentials
    # We also need to pass no API key path, otherwise Fastlane will give
    # precedence to that authentication mode.
    api_key_path = nil
  end

  update_code_signing(
    type: 'enterprise',
    # Enterprise builds belong to the "internal" team
    team_id: TEAM_ID_ENTERPRISE,
    readonly: readonly,
    app_identifiers: app_identifiers,
    api_key_path: api_key_path
  )
end

# rubocop:disable Metrics/ParameterLists
def update_code_signing(type:, team_id:, readonly:, app_identifiers:, api_key_path:, template_name: nil)
  # NOTE: It might be neccessary to add `force: true` alongside `readonly: true` in order to regenerate some provisioning profiles.
  # If this turns out to be a hard requirement, we should consider updating the method with logic to toggle the two setting based on whether we're fetching or renewing.

  # Fail early if secrets not available via `EnvManager.get_required_env!`.
  # Otherwise, Fastlane will prompt to type them.
  access_key = EnvManager.get_required_env!('MATCH_S3_ACCESS_KEY')
  secret_access_key = EnvManager.get_required_env!('MATCH_S3_SECRET_ACCESS_KEY')

  match(
    storage_mode: 's3',
    s3_bucket: 'a8c-fastlane-match',
    s3_region: 'us-east-2',
    s3_access_key: access_key,
    s3_secret_access_key: secret_access_key,
    type: type,
    team_id: team_id,
    readonly: readonly,
    app_identifier: app_identifiers,
    api_key_path: api_key_path,
    template_name: template_name
  )
end
# rubocop:enable Metrics/ParameterLists

# Compiles the array of bundle identifiers for the different targets that
# make up the Simplenote app, to be used as the `app_identifier` parameter
# for the `match` action.
def simplenote_app_identifiers(root_bundle_id: APP_STORE_BUNDLE_IDENTIFIER)
  extension_bundle_ids =
    %w[Share Widgets Intents]
    .map { |suffix| "#{root_bundle_id}.#{suffix}" }

  [root_bundle_id, *extension_bundle_ids]
end

# Compiles the dictionary mapping of the bundle identifiers and provisioning
# profiles to be used in the `export_options > provisioningProfiles`
# parameter for the `build_app`/`gym` action.
def simplenote_provisioning_profiles(root_bundle_id: APP_STORE_BUNDLE_IDENTIFIER, match_type: 'AppStore')
  # FIXME: replace the array below with the following call once established
  # the impact of adding the mapping for the Widgets extension, which is
  # something we haven't had up to this point.
  #
  # simplenote_app_identifiers(root_bundle_id: root_bundle_id)
  [root_bundle_id, "#{root_bundle_id}.Share", "#{root_bundle_id}.Intents"]
    .to_h { |key| [key, "match #{match_type} #{key}"] }
end

def prompt_user_for_app_store_connect_credentials
  require 'credentials_manager'

  # If Fastlane cannot instantiate a user, it will ask the caller for the email.
  # Once we have it, we can set it as `FASTLANE_USER` in the environment (which has lifecycle limited to this call) so that the next commands will
  # already have access to it.
  # Note that if the user is already available to `AccountManager`, setting it in the environment is redundant, but Fastlane doesn't provide a way
  # to check it so we have to do it anyway.
  ENV['FASTLANE_USER'] = CredentialsManager::AccountManager.new.user
end

########################################################################
# Localization Lanes
########################################################################

########################################################################
# Helper Lanes
########################################################################

def pull_request_number
  # Buildkite sets this env var to the PR number if on a PR, but to 'false' (and not nil) if not on a PR
  pr_num = ENV.fetch('BUILDKITE_PULL_REQUEST', 'false')
  pr_num == 'false' ? nil : Integer(pr_num)
end

def fastlane_directory
  __dir__
end

def derived_data_directory
  File.join(fastlane_directory, 'DerivedData')
end

def project_name
  'Simplenote.xcodeproj'
end

def screenshots_scheme
  'SimplenoteScreenshots'
end

def project_path
  File.join(fastlane_directory, "../#{project_name}")
end

def screenshots_directory
  File.join(fastlane_directory, 'screenshots')
end

def promo_screenshots_directory
  File.join(fastlane_directory, 'promo_screenshots')
end

def screenshot_devices
  [
    'iPhone X',
    'iPhone 8',
    'iPad Pro (12.9-inch) (2nd generation)',
    'iPad Pro (12.9-inch) (3rd generation)'
  ]
end

def simulator_version
  '14.5'
end

def deoccupy_test_account
  return if $used_test_account_index.nil?

  UI.message("Freeing used test account #{$used_test_account_index}")
  change_test_account_availability('free')
  $used_test_account_index = nil
end

# Test accounts info is stored in JSON-formatted string, where keys represent
# the account index, and values are either "free" or BUILDKITE_BUILD_NUMBER, in case if account
# was occupied by a build. Returns the hash.
# Example:
#   fetch_test_accounts_hash
#     => {"0"=>"4079", "1"=>"free", "2"=>"free", "3"=>"free"}
def fetch_test_accounts_hash
  uri = URI.parse(EnvManager.get_required_env!('UI_TESTS_ACCOUNTS_JSON_URL'))
  response = Net::HTTP.get_response(uri)
  accounts_state = response.body.chomp
  JSON.parse(accounts_state)
end

# Finds the index of first free account in accounts hash, and marks it as accupied by current build.
# Crashed fastlane if free account was not found.
def find_free_test_account
  accounts_hash = fetch_test_accounts_hash
  UI.message('Looking for a free test account...')
  UI.message("Accounts state: #{accounts_hash}")

  $used_test_account_index = accounts_hash.key('free')

  UI.user_error!('Could not find free UI Test account. Quitting.') if $used_test_account_index.nil?
  UI.message("Free account index: #{$used_test_account_index}")
  change_test_account_availability(ENV['BUILDKITE_BUILD_NUMBER'].to_s)
end

# Replace a value in a key which is equal to $used_test_account_index global variable
# Example:
#   change_test_account_availability("free")
#     => {"0"=>"free", "1"=>"free", "2"=>"free", "3"=>"free"}
#
#   change_test_account_availability("4820")
#     => {"0"=>"4820", "1"=>"free", "2"=>"free", "3"=>"free"}
def change_test_account_availability(availability)
  accounts_hash = fetch_test_accounts_hash
  accounts_hash[$used_test_account_index.to_s] = availability
  submit_accounts_hash(accounts_hash)
end

def sanitize_test_accounts
  accounts_hash = fetch_test_accounts_hash
  UI.message('Sanitizing stale test accounts...')
  UI.message("Accounts before sanitizing: #{accounts_hash}")

  accounts_hash.each do |key, value|
    next if value == 'free' || job_running?(value)

    accounts_hash[key] = 'free'
    submit_accounts_hash(accounts_hash)
  end
end

def job_running?(job_number)
  client = Buildkit.new(token: EnvManager.get_required_env!('BUILDKITE_TOKEN'))
  build = client.build('automattic', 'simplenote-ios', job_number)

  state = build['state']

  UI.message("Status of Job Number #{job_number} is #{state}")

  build['state'] == 'running'
rescue StandardError
  false
end

def submit_accounts_hash(accounts_hash)
  UI.message("Writing new accounts state: #{accounts_hash}")
  uri = URI.parse(EnvManager.get_required_env!('UI_TESTS_ACCOUNTS_JSON_URL'))
  request = Net::HTTP::Post.new(uri)
  request.body = accounts_hash.to_json

  req_options = {
    use_ssl: uri.scheme == 'https'
  }

  Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
    http.request(request)
  end

  UI.message("Accounts state after write: #{fetch_test_accounts_hash}")
end

# Returns the release version of the app in the format `1.2` or `1.2.3` if it is a hotfix
#
def release_version_current
  current_version = VERSION_FORMATTER.parse(PUBLIC_VERSION_FILE.read_release_version)
  VERSION_FORMATTER.release_version(current_version)
end

# Returns the next release version of the app in the format `1.2` or `1.2.3` if it is a hotfix
#
def release_version_next
  current_version = VERSION_FORMATTER.parse(PUBLIC_VERSION_FILE.read_release_version)
  next_calculated_release_version = VERSION_CALCULATOR.next_release_version(version: current_version)
  VERSION_FORMATTER.release_version(next_calculated_release_version)
end

# Returns the current build code of the app
#
def build_code_current
  # We use the four part (1.2.3.4) build code format, so the version calculator can be used to calculate the next four-part version
  version = VERSION_FORMATTER.parse(PUBLIC_VERSION_FILE.read_build_code(attribute_name: 'VERSION_LONG'))
  BUILD_CODE_FORMATTER.build_code(version: version)
end

# Returns the initial build code for a code freeze (e.g., "1.2.3.0" for release version "1.2.3")
# Takes the release version and formats it as a four-part build code with the build number set to 0
#
def build_code_code_freeze(version_short: nil)
  # Use provided version or read the current release version from the .xcconfig file
  version_short ||= PUBLIC_VERSION_FILE.read_release_version
  # Parse the release version string (e.g., "1.2.3") into an AppVersion object
  release_version_current = VERSION_FORMATTER.parse(version_short)
  # Format as four-part build code (e.g., "1.2.3.0")
  BUILD_CODE_FORMATTER.build_code(version: release_version_current)
end

# Returns the build code of the app for the code freeze.
# It is the release version name with build number (last of the four components) set to 0.
#
def build_code_hotfix(release_version:)
  version = VERSION_FORMATTER.parse(release_version)
  BUILD_CODE_FORMATTER.build_code(version: version)
end

# Returns the next build code of the app
#
def build_code_next
  # We use the four part (1.2.3.4) build code format, so the version calculator can be used to calculate the next four-part version
  build_code_current = VERSION_FORMATTER.parse(PUBLIC_VERSION_FILE.read_build_code(attribute_name: 'VERSION_LONG'))
  build_code_next = VERSION_CALCULATOR.next_build_number(version: build_code_current)
  BUILD_CODE_FORMATTER.build_code(version: build_code_next)
end

def release_branch_name(release_version: release_version_current)
  "#{RELEASE_BRANCH_ROOT}#{release_version}"
end

def ensure_git_branch_is_release_branch!
  # Verify that the current branch is a release branch.
  # Notice that `ensure_git_branch` expects a RegEx parameter.
  # Also, ensure_git_branch will fail the lane if the branch doesn't match, hence the ! in the method name.
  ensure_git_branch(branch: "^#{RELEASE_BRANCH_ROOT}")
end

RELEASE_BRANCH_ROOT = 'release/'
